Документация Taiga IoT — зона администратора
Инструкция по серверу, GitLab, CI и сабмодулям
Внутренний сайт (Admin): https://95.183.13.164.sslip.io:3002/
Страница входа: https://auth.95.183.13.164.sslip.io:3002/
Публичный сайт (User): http://95.183.13.164:3003/
GitLab: https://gitlab.taigaiot.com/
Umbrella: https://gitlab.taigaiot.com/taiga_docs/taigadocs/taigadocs-internal (project ID 177)
Гайд для технического писателя:
guide-to-OBSIDIAN-for-technical-writers
1. Назначение системы
TaigaDocs — публикация технической документации двумя сайтами с одного набора статей.
Документация продукта лежит в своём GitLab-репозитории. В репозитории сайта эти продукты подключены как git submodules (вложенные репозитории). Путь submodule зеркалит путь проекта в GitLab: docs/<CI_PROJECT_PATH>. Например, репозиторий taiga_docs/taigabadgelw/docs лежит в umbrella как docs/taiga_docs/taigabadgelw/docs. Docusaurus собирает их в один сайт.
Техпис работает только с репозиторием продукта и Obsidian. Сервер, общий репозиторий и публичный сайт ему не нужны.
Техпис
↓ Commit + Push в репозиторий продукта
Product CI
↓ переписывает картинки/PDF Obsidian
↓ trigger API
GitLab: TaigaDocs (project 177)
↓ sync_submodule — путь docs/<TRIGGERED_BY>, запоминает SHA
↓ deploy_docs
Docker на 95.183.13.164
↓
Admin https://95.183.13.164.sslip.io:3002/ (сразу)
User http://95.183.13.164:3003/ (только после тега docs-*)
Рабочая папка администратора на ПК: C:/taiga-docs
Это клон родительского репозитория сайта (umbrella). Техпис клонирует не её, а репозиторий продукта.
2. Словарь
| Термин | Что это |
|---|---|
| Umbrella | Родительский репозиторий taiga_docs/taigadocs/taigadocs-internal. В нём сайт, CI и список продуктов |
| Product | Отдельный репозиторий продукта с Markdown |
| Submodule | Репозиторий продукта, подключённый внутрь umbrella |
| Path-mirror | Правило пути: submodule = docs/ + путь проекта в GitLab (CI_PROJECT_PATH) |
| Gitlink | Запись в umbrella: «этот продукт зафиксирован на таком-то SHA» |
| SHA | Идентификатор одного коммита |
| CI | Автоматические действия GitLab после push |
| Pipeline | Один запуск CI |
| Job | Одна задача внутри pipeline |
| Runner | Машина, которая выполняет jobs. У нас это сам сервер сайта |
| Docusaurus | Сборщик статического сайта из Markdown |
| Authelia | Страница логина и сессии внутреннего сайта |
| doc_rev | Короткий номер версии текста статьи. Его считает сборка сайта |
| Allowlist | Список путей, которые можно показать на публичном сайте (allowlist-public.yml) |
Тег docs-* | Метка на коммите umbrella. Только она обновляет публичный сайт |
| Orphan | Файл-вложение в папке статей, на который ни одна статья не ссылается |
Часть A. Карта системы
A1. Два сайта
| Admin | User | |
|---|---|---|
| Зачем | Вся документация для своих. Сюда попадает push техписа | То, что уже решили показать наружу |
| Адрес | https://95.183.13.164.sslip.io:3002/ | http://95.183.13.164:3003/ |
| Вход | Логин Authelia | Нет |
| Откуда текст | ветка main umbrella | последний тег docs-* |
| Каталог на сервере | /opt/docusaurus-docs | /opt/docusaurus-docs-user |
| Репозиторий | taiga_docs/taigadocs/taigadocs-internal | тот же umbrella; публичный каталог собирается из шаблона + allowlist |
Push техписа меняет только Admin. User в этот момент остаётся на прошлом теге. Это не поломка.
Сертификат Admin самоподписанный (в образ его кладёт Dockerfile, в имени сертификата есть IP и 95.183.13.164.sslip.io). Браузер показывает предупреждение — для этой лаборатории это нормально. Позже, когда будет свой домен, сертификат меняется на Let's Encrypt.
Почему не голый IP в адресной строке: сессия Authelia хранится в cookie, а cookie с доменом на голый IP не сажается. sslip.io — временное имя, которое само указывает на этот IP. Когда появится домен, в конфиге меняются два имени: сайт и auth.….
Кнопка Log out в шапке ведёт на
https://auth.95.183.13.164.sslip.io:3002/logout?rd=…
и сбрасывает сессию. Старый адрес http://IP:3002/ документацию больше не отдаёт.
TLS внутреннего сайта висит на 3002 → 443 внутри контейнера.
A2. Репозитории GitLab
Umbrella: taiga_docs/taigadocs/taigadocs-internal — project ID 177
Документация продуктов живёт в группе taiga_docs (и при необходимости в других группах). Живой список submodule — файл .gitmodules. Если эта таблица и .gitmodules разошлись, верьте .gitmodules.
| Репозиторий GitLab | Путь submodule |
|---|---|
taiga_docs/taigabadgelw/docs | docs/taiga_docs/taigabadgelw/docs |
taiga_docs/toesnode/docs | docs/taiga_docs/toesnode/docs |
taiga_docs/taigabadgelw/software | docs/products/taigabadgelw/Software |
taiga_docs/taigabadgelw/firmware | docs/products/taigabadgelw/Firmware |
taiga_docs/toesnode/software | docs/products/toesnode/Software |
taiga_docs/toesnode/firmware | docs/products/toesnode/Firmware |
taiga_docs/group/test | docs/taiga_docs/group/test |
taiga_docs/group/test-files | docs/taiga_docs/group/test-files |
taiga_docs/group/test-permission | docs/taiga_docs/group/test-permission |
taiga-products/ng_test_product | docs/taiga-products/ng_test_product |
Как читать пути. Для новых репозиториев с документацией правило одно: путь в umbrella = docs/ + путь проекта в GitLab. Старые Software/Firmware пока ещё лежат под docs/products/.... Sync при следующем trigger сам переносит submodule на path-mirror, если он был зарегистрирован по старому пути.
A3. Что лежит в umbrella
taigadocs-internal/
├── docs/ ← статьи самого сайта + submodule продуктов
│ ├── taiga_docs/.../ ← path-mirror (docs/<gitlab path>)
│ └── products/.../ ← часть старых Software/Firmware
├── .gitmodules
├── .gitlab-ci.yml
├── allowlist-public.yml ← что можно показать на User
├── ci/product-trigger.gitlab-ci.yml
├── authelia/configuration.yml ← логин; users_database.yml на сервере, не в git
├── docker-compose.yml ← Admin: docs + authelia + redis + history-api
├── docker-compose.lockdown.yml ← рудемент
├── templates/taiga-docs-user/ ← шаблон публичного сайта
└── scripts/
├── ci-sync-submodule.sh
├── ci-deploy-compose.sh
├── ci-deploy-public.sh
├── convert-obsidian-embeds.sh
├── bump-doc-revs.cjs
├── prepare-docs-build.cjs
├── copy-raw-docs.cjs
├── generate-files-list-sidebar.cjs
├── check-doc-images.sh
├── check-doc-links.cjs
├── sanitize-svg.sh
└── backup-admin-stack.sh
ci-sync-submodule.sh сам строит путь docs/<TRIGGERED_BY>. Отдельный case на каждый продукт не нужен. Если репозитория ещё нет в .gitmodules, sync может добавить его сам. Если репозиторий в GitLab удалён (HTTP 404), sync может убрать мёртвый submodule.
A4. Сервер
| Параметр | Значение |
|---|---|
| IP | 95.183.13.164 |
| Имя для cookie | 95.183.13.164.sslip.io |
| Каталог Admin | /opt/docusaurus-docs |
| Каталог User | /opt/docusaurus-docs-user |
| Runner | shell с тегом docs_server_instance на этом же VPS |
| Диск | около 59 ГБ. Кэшу Docker больше 2–4 ГБ не оставлять |
Контейнеры Admin (docker compose ps в /opt/docusaurus-docs):
| Контейнер | Снаружи | Зачем |
|---|---|---|
docusaurus-docs-docs-1 | 3002 → 443 | nginx, статика, TLS, проверка входа, прокси истории |
docusaurus-docs-authelia-1 | нет, порт 9091 только внутри сети Docker | логин и сессии |
docusaurus-docs-redis-1 | нет | сессии Authelia в памяти, на диск не пишет |
docusaurus-docs-history-api-1 | нет | история статьи из GitLab |
Публичный сайт — отдельный compose в /opt/docusaurus-docs-user, контейнер на 3003 → 80, без Authelia.
Как устроен запрос к Admin:
Браузер
│ HTTPS :3002, имя *.sslip.io
▼
nginx в контейнере docs
│ «этот человек уже вошёл?»
▼
Authelia
│ сессия лежит в Redis
▼
nginx отдаёт страницу Docusaurus
Пароль Authelia сверяет с хешем в authelia/users_database.yml. Redis нужен только как список уже начатых сессий.
Секреты — в /opt/docusaurus-docs/.env, не в git:
| Переменная | Зачем |
|---|---|
AUTHELIA_JWT_SECRET | служебные токены Authelia |
AUTHELIA_SESSION_SECRET | подпись сессии |
AUTHELIA_STORAGE_ENCRYPTION_KEY | шифрование хранилища Authelia |
DOCS_HISTORY_TOKEN | чтение GitLab для кнопки истории. Без него деплой останавливается |
Первый пользователь admin и эти три ключа Authelia, если их ещё нет:
cd /opt/docusaurus-docs
bash scripts/authelia-bootstrap.sh 'ВашПароль'
users_database.yml на сервере есть, в git его нет. Не коммитьте его и не копируйте пароль в эту инструкцию.
Перед сменой портов, TLS, Authelia или lockdown сначала снимок:
cd /opt/docusaurus-docs
bash scripts/backup-admin-stack.sh pre-change
# → /root/backups/taiga-docs-admin/<дата>-pre-change/
Ручная пересборка, если нужно повторить то, что делает CI:
cd /opt/docusaurus-docs
bash scripts/ci-deploy-compose.sh
Скрипт не делает docker rm, а потом долгую сборку. Он сначала собирает образ, потом освобождает имена docusaurus-docs-*-1, потом запускает уже собранное. Если удалить контейнеры и только затем собирать, имя может занять другой процесс, и up упадёт с «container name already in use».
Проверка:
docker compose ps
В конце скрипт печатает, что Authelia поднята, и напоминает адрес https://95.183.13.164.sslip.io:3002/.
Диск и кэш сборки CI крутится на этом же VPS. Каждая сборка образа пишет кэш BuildKit (часто 150–400 МБ). Без лимита за несколько дней диск 59 ГБ заканчивается, хотя «живые» контейнеры маленькие.
Лимит уже стоит в двух местах: docker builder prune --keep-storage=2GB внутри scripts/ci-deploy-compose.sh и GC в /etc/docker/daemon.json (builder.gc.defaultKeepStorage: 2GB). Ручной docker builder prune -af без лимита диск освободит один раз; следующий день бампов набьёт его снова.
Крупное на диске, кроме Docker: /root/backups (старые дампы), MySQL taiga_projects, /root/.vscode-server. Хостовый node_modules в /opt/docusaurus-docs для работы сайта не нужен: сайт собирается в Docker.
Закрыть Admin от интернета, оставив его только на сервере:
docker compose -f docker-compose.yml -f docker-compose.lockdown.yml up -d
Тогда порт 3002 слушает 127.0.0.1. Снаружи его нет, с сервера и через SSH-туннель — есть. Cookie всё равно требует имя *.sslip.io, не 127.0.0.1.
A5. Логин и пароль
Логин есть только у TaigaAdmin (https://95.183.13.164.sslip.io:3002/).
Публичный сайт http://95.183.13.164:3003/ пароль не спрашивает.
Страницу входа рисует Authelia. nginx сайта сам статью не отдаёт: сначала спрашивает Authelia, есть ли у браузера живая сессия. Если нет — браузер уходит на страницу авторизации https://auth.95.183.13.164.sslip.io:3002/.
Пароль в открытом виде нигде не хранится. В файле на сервере лежит хеш (argon2). Redis пароль не знает: в нём только уже открытые сессии, и только в памяти. После перезапуска Redis все сессии пропадают, люди входят заново.
Файл учёток: /opt/docusaurus-docs/authelia/users_database.yml.
В git его нет. В коммит его не класть.
Кто может войти: любой пользователь из этого файла, у которого пароль подошёл. Отдельных прав «только этот продукт» у логина нет. Группа admins в файле записана, но сайт по ней доступ не режет.
Сессия:
После входа Authelia отдаёт браузеру cookie authelia_session. По ней nginx понимает, что этот человек уже входил. Cookie привязана к имени 95.183.13.164.sslip.io. Браузер не сохраняет такую cookie на голый IP, поэтому Admin открывают как https://95.183.13.164.sslip.io:3002/, а не как https://95.183.13.164:3002/.
Сессия живёт не бесконечно:
- 4 часа никто не открывал сайт — сессия кончается. Следующий заход снова спросит пароль.
- Сайтом пользуются без перерыва — всё равно через 12 часов после входа сессия кончается, и пароль спрашивают снова.
Поэтому Admin открывают по имени sslip.io, а не по голому IP. На голый IP такая cookie не сажается.
Кнопка Log out в шапке сбрасывает сессию. Тот же адрес можно открыть самому:
https://auth.95.183.13.164.sslip.io:3002/logout
Письмо «забыл пароль» никуда не уходит. Уведомления Authelia пишет в файл внутри контейнера. Пароль меняют на сервере.
Первый пользователь
Если users_database.yml ещё нет, его создаёт скрипт из A4:
cd /opt/docusaurus-docs
bash scripts/authelia-bootstrap.sh 'ВашПароль'
Он же дописывает в .env три ключа Authelia, если их ещё не было. После этого нужен деплой, как написано в выводе скрипта.
Сменить пароль единственного admin
Тот же скрипт перезаписывает весь users_database.yml и оставляет в нём одного пользователя admin.
Так делать можно, пока в файле только он. Если там уже есть другие люди, скрипт их сотрёт.
Старые сессии в Redis от смены хеша сами не умирают. Человек с ещё живой cookie остаётся внутри, пока не выйдет или пока не кончатся 4 / 12 часов. Выкинуть всех сейчас:
cd /opt/docusaurus-docs
docker compose restart redis
Новый вход уже проверяется по новому хешу. Файл смонтирован в контейнер, Authelia следит за ним (watch: true), отдельная пересборка сайта для смены пароля не нужна.
Добавить ещё одного человека
Bootstrap для этого не запускать.
На сервере, в том же каталоге:
cd /opt/docusaurus-docs
docker run --rm authelia/authelia:4.38.19 \
authelia crypto hash generate argon2 --password 'ПарольЭтогоЧеловека'
В вывод попадет строка Digest:. В файл учёток копируют только сам хеш, без слова Digest:. Хеш в кавычках: в нём есть $.
В authelia/users_database.yml добавляют блок рядом с admin, файл целиком не заменяют:
ivan:
disabled: false
displayname: 'Ivan'
password: '$argon2id$…хеш…'
email: ivan@taigaiot.com
groups:
- admins
Имя слева (ivan) — это логин. Пароль в чат и в git не копировать.
Отключить человека, не стирая блок: disabled: true.
Часть B. GitLab CI/CD
B1. Runner docs_server_instance
Group / Project → Settings → CI/CD → Runners
Runner стоит на 95.183.13.164 и выполняет команды прямо на сервере. Поэтому deploy_docs видит /opt/docusaurus-docs и Docker этой машины.
В jobs указан тег:
tags:
- docs_server_instance
Если раннер выключен или проекту его не выдали, job висит в Pending.

B2. Переменные CI/CD
Хранить только в Settings → CI/CD → Variables. Не в .gitlab-ci.yml, не в скриптах, не в .gitmodules, не в коммите.
Группа / проект:
| Переменная | Зачем | Где нужна |
|---|---|---|
DOCS_TRIGGER_TOKEN | product CI запускает pipeline проекта 177 | продукт и группа |
GITLAB_PULL_TOKEN | read_repository: деплой читает umbrella и все продукты | umbrella |
DOCS_PUSH_TOKEN | write_repository на umbrella: записать новый SHA продукта | umbrella |
DOCS_HISTORY_TOKEN | read_api: кнопка истории. Дублируется в .env на сервере | umbrella |
По желанию:
| Переменная | Зачем |
|---|---|
DOCS_PRODUCT_PUSH_TOKEN | push обратно в продукт, когда CI переписал картинки Obsidian. Если пусто, CI пробует job token. Ошибка push не роняет pipeline: картинки всё равно перепишутся при сборке Admin |
DOCS_SITE_DIR | каталог Admin на сервере, по умолчанию /opt/docusaurus-docs |
DOCS_USER_SITE_DIR | каталог User на сервере, по умолчанию /opt/docusaurus-docs-user |
DOCS_USER_PUSH_TOKEN для текущего выпуска не нужен: публичный сайт собирается на сервере из тега umbrella, во второй Git-репозиторий ничего не пушится.
Откуда берётся DOCS_TRIGGER_TOKEN
Проект 177 → Settings → CI/CD → Pipeline trigger tokens.
Создать токен и положить значение в групповую переменную DOCS_TRIGGER_TOKEN.
Права остальных токенов
GITLAB_PULL_TOKEN—read_repositoryна umbrella и на каждый продукт, который деплой подтягивает.DOCS_PUSH_TOKEN—write_repositoryнаtaiga_docs/taigadocs/taigadocs-internal.DOCS_HISTORY_TOKEN—read_apiна umbrella и на репозитории продуктов (история статей).
Часть C. CI продукта
C1. Один шаблон на все продукты
В корне продукта только:
include:
- project: 'taiga_docs/taigadocs/taigadocs-internal'
ref: main
file: '/ci/product-trigger.gitlab-ci.yml'
Логика меняется в ci/product-trigger.gitlab-ci.yml umbrella. Десятки копий в продуктах не правят.
C2. Две задачи, не сборка сайта
Product CI не собирает Docusaurus и не деплоит.
Стадии:
convert_obsidian_embeds
↓
trigger_docs_rebuild
convert_obsidian_embeds на раннере:
- Берёт скрипт из
/opt/docusaurus-docs(если каталог пуст — скачивает его из проекта 177). convert-obsidian-embeds.sh
![[photo.png]]→
![[scheme.svg]]→ файл вdiag/
![[manual.pdf]]→ окно просмотра и ссылка на/files/docs/...
Ссылки вида[[другая-статья]]скрипт не трогает. Примеры в блоках кода гайда пропускает.- Если что-то изменилось — коммит и push в
mainпродукта сci.skip, чтобы не запустить второй такой же pipeline. - В umbrella уходит SHA после этих коммитов, если push удался. Если push не удался, уходит SHA коммита техписа, а переписывание картинок повторит
deploy_docs.
Номер doc_rev product CI не пишет. Его считает сборка Admin (и User) в копии дерева .docs-build при npm run build / Docker-сборке. В репозиторий продукта этот номер из сборки обратно не коммитится.
Дальше trigger_docs_rebuild вызывает Trigger API проекта 177 и передаёт:
| Переменная | Значение |
|---|---|
TRIGGERED_BY | путь проекта, например taiga_docs/taigabadgelw/docs |
SUBMODULE_SHA | SHA, который надо зафиксировать |
Путь submodule в umbrella будет docs/<TRIGGERED_BY>.
Часть D. Pipeline umbrella — проект 177
Стадии: sync → deploy → publish_user.
publish_user в обычном push нет. Он есть только у тега.
D1. sync_submodule
Скрипт scripts/ci-sync-submodule.sh. Запускается, только если pipeline пришёл от trigger и передан SUBMODULE_SHA.
TRIGGERED_BY
↓
путь docs/<TRIGGERED_BY>
↓ (если нет — add; если был старый путь — relocate)
gitlink = SUBMODULE_SHA
↓
commit + push в main umbrella с ci.skip
Сообщение коммита похоже на chore(docs): bump … to 3c69f1f.
Push с ci.skip не запускает второй pipeline. Иначе получилась бы петля trigger → sync → push → trigger. Сборка идёт в том же pipeline, который запустил продукт.
Нормальная картина:
Pipeline 177 (его создал продукт)
├── sync_submodule Passed
└── deploy_docs Passed
Отдельный pipeline у коммита bump
└── Skipped ← так и должно быть
Skipped у bump-коммита — не ошибка, если исходный pipeline дошёл до deploy_docs.
Прямой push в main umbrella (правка этого гайда, CI, тема) не вызывает sync_submodule. Сразу начинается deploy_docs.
D2. deploy_docs
Обновляет клон на сервере и собирает Admin.
git fetch
git reset --hard origin/main
git submodule update --init --recursive --force
git clean -ffd -- docs/ (очищаем хвосты на серве(если какие-то репо были del))
convert-obsidian-embeds.sh на всё дерево docs/
sanitize-svg.sh
check-doc-images.sh
scripts/ci-deploy-compose.sh
→ prepare-docs-build (копия + doc_rev)
→ copy-raw-docs (/raw/docs/…)
→ generate-files-list («Файлы» + _files-all.zip)
→ check-doc-links
→ docusaurus build
git submodule update берёт SHA из gitlink. Флага --remote нет: сайт не подтягивает «просто последний» коммит продукта, только тот, который записал sync.
check-doc-images.sh останавливает публикацию, если картинка не найдена или в статье осталась живая вставка ![[…]] вне блока кода. Примеры в гайде техписа на это не падают: они в обратных кавычках или в блоке кода.
Номер doc_rev появляется на сайте при сборке. Техпису он нужен для поиска: на сайте его можно скопировать, поиск ищет и по нему.
D3. Почему SHA в bump может быть не тем, что пушил техпис
У продукта и umbrella разные коммиты.
Коммит техписа в продукте: aaaaaaa
Коммит CI (картинки Obsidian): bbbbbbb ← его часто и фиксирует umbrella
Коммит umbrella: ccccccc chore(docs): bump … to bbbbbbb
Если push из product CI прошёл, в bump будет bbbbbbb. Искать «пропавший» коммит техписа нужно в истории продукта: он родитель коммита CI.
Часть E. Что видит читатель на Admin
После входа у статьи есть:
- поиск над деревом слева (в шапке строка поиска спрятана);
- скачивание
.mdс адреса/raw/docs/...; - Другое → Export and download to PDF (печать браузера);
- история правок через
/api/history/(контейнер history-api иDOCS_HISTORY_TOKEN); - блок
doc_revи кнопка Копировать; - страница Файлы в папках, где лежат «осиротевшие» вложения;
- Log out.
История продукта ходит в репозиторий продукта. История статьи самого umbrella — в проект 177, путь с пометкой _umbrella.
Страница «Файлы» и архив
Если в папке со статьями лежат вложения (pdf, docx, xlsx, bin, zip, hex, txt, 7z, rar), на которые ни одна статья этой папки не ссылается, сборка считает их orphan — «осиротевшими».
Для такой папки сборка делает:
- страницу
_files.mdв сайдбаре с названием Файлы — таблица ссылок на скачивание; - архив
static/files/docs/<путь>/_files-all.zip— кнопка «Скачать всё архивом».
Файлы, которые уже вставлены в статью (![[manual.pdf]] и т.п.), в «Файлы» и в zip не попадают: они уже привязаны к тексту. Страница и zip живут только в сборке сайта, в репозиторий продукта их не коммитят.
Порядок в npm run build: сначала copy-raw-docs копирует вложения в static/files/docs, потом generate-files-list-sidebar пишет _files.md и zip. Иначе очистка static/files сотрёт только что собранный архив.
E2. Две версии документации
Слово «версия» здесь про две разные вещи.
Номер статьи, doc_rev.
Короткий код из 7 символов. Его считает сборка сайта, не техпис и не админ. Скрипт смотрит отпечаток текста и локальных картинок. Текст не менялся — номер тот же. Текст или картинка изменились — пишется новый номер. Дата в doc_rev_at и тот же код в keywords нужны, чтобы поиск находил статью по номеру. На сайт для читателя снаружи этот номер сам по себе ничего не выпускает.
Снимок публичного сайта, тег docs-*.
Подпись на одном коммите umbrella. User показывает последний такой тег, а не сегодняшний main, то есть версию репозитория в момент создания тега. Между двумя тегами на Admin номера doc_rev могут смениться много раз. Наружу уезжает тот текст и те номера, которые были в коммите тега.
Откатить изменения на публичном сайте — повторить publish_user с тем тегом, до версии которого нужно откатиться. Admin от этого на старый коммит не встаёт.
Часть F. Полный рабочий процесс
F1. Техпис изменил существующий продукт
Obsidian → commit → push main продукта
↓
convert_obsidian_embeds
↓
trigger_docs_rebuild
↓
Pipeline 177: sync_submodule → deploy_docs
↓
Admin обновился
User не изменился
Пока идёт deploy_docs, второй деплой того же сайта ждёт: у job стоит resource_group: docs-production. Поэтому «подождите пару минут» иногда превращается в дольше.
F2. Выпуск на User
Admin показывает последний main. User показывает последний тег docs-* на umbrella.
Тег — подпись на одном конкретном коммите umbrella. В этом коммите уже записаны SHA всех продуктов. Отдельные теги в репозиториях продуктов не нужны.
Один раз в проекте taiga_docs/taigadocs/taigadocs-internal:
Settings → Repository → Protected tags
Шаблон: docs-*. Создавать может Maintainer. Техпис с правом push в продукт этот тег создать не может. Из репозитория эта защита не включается.
Как выпустить:
- Дождаться зелёного pipeline Admin.
- Deploy → Releases → New release.
- Имя тега:
docs-ГГГГ-ММ-ДД. Источник:main. - Pipeline этого тега запускает только
publish_user.deploy_docsна теге не бежит, Admin не откатывается. - Job
scripts/ci-deploy-public.shберёт этот тег в отдельный каталог, не живую папку/opt/docusaurus-docs. Подтягивает продукты тех SHA, которые в теге, копирует allowlist в шаблон User и собирает/opt/docusaurus-docs-user. Во второй Git ничего не пушится. - На
:3003поднимается публичный сайт.
Что смотреть в allowlist — файл allowlist-public.yml. Если абзац ниже и файл разойдутся, верьте файлу. Сейчас в include есть деревья docs/products/taigabadgelw/**, docs/products/toesnode/**, зеркала docs/taiga_docs/taigabadgelw/**, docs/taiga_docs/toesnode/**, плюс docs/intro.md и _category_.json.
Гайды техписа и админа, .env, .obsidian на User не копируются. Чтобы показать снаружи другой продукт, его путь добавляют в include этого файла и выпускают новый тег. Push техписа сам файл allowlist не расширяет.
Откат User: в pipeline старого тега повторить job publish_user. Admin остаётся на main.
Проверка, что контур живой:
- push в продукт меняет
:3002и не трогает:3003; - New release создаёт
publish_userи обновляет:3003; - следующий push снова двигает только Admin.
F3. Статья на публичном сайте
Push техписа обновляет только Admin. На http://95.183.13.164:3003/ статья появляется после тега docs-*, и только если её путь разрешён в allowlist-public.yml.
Как поставить тег — в F2. Здесь — решить, попадёт ли эта статья в копирование.
Статья уже внутри разрешённого продукта
Новая страница внутри уже включённого дерева (**) в allowlist не дописывается. Шаблон забирает всё дерево продукта, которое записано в теге.
Порядок:
-
Дождаться, пока статья есть на Admin и pipeline umbrella зелёный. В коммите umbrella уже должен быть SHA продукта с этой статьёй.
-
Поставить тег
docs-*на этотmain, как в F2. -
Создать релиз с этим тегом.
Тег раньше, чем sync_submodule записал новый SHA, скопирует старое дерево продукта. Статьи на User не будет, хотя на Admin она уже открывается.
Статья или продукт в allowlist ещё не входят
Сначала правка umbrella, потом тег. Правка только на сервере не живёт: следующий deploy_docs делает reset --hard.
В include добавляют строку:
| Что показать | Строка |
|---|---|
| Весь продукт | docs/<gitlab-path>/** |
| Один файл | docs/<gitlab-path>/папка/статья.md |
| Файлы одной папки, без вложенных | docs/<gitlab-path>/папка/*.md |
Одна строка на .md копирует только этот файл. Картинки рядом и _category_.json этой папки сами не приедут, меню продукта на User соберётся дырявым. Для продукта проще одна строка на всё дерево.
exclude сильнее include. Даже внутри ** на User не попадают:
-
**/internal/** -
**/.obsidian/** -
гайды
docs/guide to *.md -
.env,CHAT_CONTEXT.md,.gitlab-ci.yml
Дальше:
-
Commit и push umbrella.
-
Дождаться, что этот коммит на
main. Если вместе с ним должен уехать и свежий текст продукта — зелёныйdeploy_docsс нужным SHA, как в пункте выше. -
Тег
docs-*на этотmainи релиз.
publish_user берёт дерево тега, не живую папку /opt/docusaurus-docs, оставляет только allowlist и собирает User на сервере. Проверка: страница открывается на :3003. Гайда админа и гайда техписа там быть не должно.
Часть G. Добавление нового продукта
Добавление нового продукта выполняет администратор.
Техническому писателю после настройки нужно только получить доступ к product-репозиторию и работать через Obsidian.
G1. Чеклист
| # | Действие | Где |
|---|---|---|
| 1 | Создать product-репозиторий | GitLab |
| 2 | Создать .gitlab-ci.yml с центральным include | Product |
| 3 | Проверить доступ runner docs_server_instance | GitLab |
| 4 | Проверить Group CI/CD Variables | GitLab |
| 5 | Сделать тестовый push в product | Product |
| 6 | Дождаться sync + deploy в проекте 177 | GitLab / браузер |
| 7 | При необходимости добавить путь в allowlist-public.yml | Umbrella |
Руками добавлять submodule в umbrella обычно не нужно: sync_submodule сам создаст путь docs/<CI_PROJECT_PATH> и промежуточные _category_.json.
G2. Создать product-репозиторий
В нужной группе GitLab создать новый проект. Пример:
taiga_docs/новый-продукт/docs
Основная ветка:
main
После первого успешного trigger submodule окажется здесь:
docs/taiga_docs/новый-продукт/docs
G3. Настроить .gitlab-ci.yml
В корне product-репозитория создайте файл:
.gitlab-ci.yml
внутри должно быть:
include:
- project: 'taiga_docs/taigadocs/taigadocs-internal'
ref: main
file: '/ci/product-trigger.gitlab-ci.yml'
G4. Проверить Runner
У нового product должен быть доступен runner с тегом:
docs_server_instance
Если runner групповой, достаточно убедиться, что проект имеет право его использовать.

G5. Проверить CI/CD Variables
Для product должен быть доступен:
DOCS_TRIGGER_TOKEN
Найти его можно в настройках CI/CD umbrella (проект 177) в разделе Pipeline trigger tokens. Значение кладут в CI/CD Variables группы или продукта.
Остальные токены (GITLAB_PULL_TOKEN, DOCS_PUSH_TOKEN) используются в TaigaDocs CI/CD и не требуют добавления в каждый product отдельно, если они уже настроены на уровне группы.
Часть H. Подключение продукта как submodule
Обычный путь — push в продукт с готовым include. Sync сам:
- вычислит
docs/<CI_PROJECT_PATH>; - создаст родительские папки и
_category_.json, если их ещё нет; - добавит submodule или перенесёт его со старого пути;
- запишет SHA и запушит bump в umbrella с
ci.skip.
Если всё же нужно добавить submodule вручную с ПК администратора:
cd C:\taiga-docs
git pull origin main
Добавление (путь должен совпасть с будущим path-mirror):
git submodule add -b main <URL_репозитория> docs/<gitlab-group/.../project>
Пример:
git submodule add -b main https://gitlab.taigaiot.com/taiga_docs/новый-продукт/docs.git docs/taiga_docs/новый-продукт/docs
После этого проверить:
git status
git diff -- .gitmodules
В .gitmodules должна появиться запись вида:
[submodule "docs/taiga_docs/новый-продукт/docs"]
path = docs/taiga_docs/новый-продукт/docs
url = https://gitlab.taigaiot.com/taiga_docs/новый-продукт/docs.git
branch = main
Затем:
git add .gitmodules docs/taiga_docs/новый-продукт/docs
git commit -m "docs: add новый-продукт docs submodule"
git push origin main
Путь submodule должен быть стабильным и совпадать с docs/<CI_PROJECT_PATH>.
Не переименовывать путь в папку с пробелами или «красивым» отображаемым названием.
Отображаемое название продукта задаётся в Docusaurus/Markdown, а путь submodule остаётся техническим.
Важно:
У продукта и umbrella разные Git-коммиты.
Например:
Product:
3c69f1f
Umbrella:
d1e4c94
3c69f1f — commit в репозитории продукта.
d1e4c94 — commit umbrella, который зафиксировал:
docs/taiga_docs/taigabadgelw/docs → 3c69f1f
Поэтому при диагностике всегда нужно понимать, какой SHA проверяется.
Часть I. Проверка, если «на сайте старое»
I1. Продукт
Product → Build → Pipelines. Должны пройти convert_obsidian_embeds и trigger_docs_rebuild.
Если pipeline нет или он Pending:
- push был не в
main; - нет
.gitlab-ci.ymlс include; - нет
DOCS_TRIGGER_TOKEN; - раннер
docs_server_instanceне выдан проекту.
Предупреждение push failed внутри convert — ещё не провал всего pipeline. Смотрите, дошёл ли trigger. Картинки тогда перепишутся на шаге Admin.
I2. Umbrella
В проекте 177 pipeline от Trigger API:
sync_submodule Passed
deploy_docs Passed
publish_user в этом списке быть не должно.
В истории umbrella ищите коммит вида chore(docs): bump … to 3c69f1f: имя/путь продукта и короткий SHA.
На ПК админа, в C:/taiga-docs, после git pull:
git submodule status
Строка продукта должна показывать тот SHA, который ушёл в trigger. Помните часть D3: это может быть коммит CI, а не коммит техписа.
I3. Сервер
cd /opt/docusaurus-docs
git rev-parse --short HEAD
docker compose ps
HEAD должен совпасть с main, на котором закончился deploy_docs. Контейнер docs — Up, Authelia — healthy.
Открывать проверку нужно как https://95.183.13.164.sslip.io:3002/, не как http://IP:3002.
Часть J. Типичные ошибки
| Симптом | Что проверить |
|---|---|
Product pipeline Pending | Runner docs_server_instance не assigned |
DOCS_TRIGGER_TOKEN is not set | Групповая переменная или trigger token проекта 177 |
| Продукт зелёный, pipeline 177 не появился | URL trigger, токен, project id 177 |
sync_submodule не находит / не добавляет продукт | TRIGGERED_BY = CI_PROJECT_PATH, права GITLAB_PULL_TOKEN / DOCS_PUSH_TOKEN |
deploy_docs падает на git | Каталог /opt/docusaurus-docs, GITLAB_PULL_TOKEN |
deploy_docs падает на submodule | SHA существует, токену хватает read_repository на этот продукт |
DOCS_HISTORY_TOKEN is empty | Переменная CI и/или .env. Пустой export DOCS_HISTORY_TOKEN= в shell перекрывает .env |
container name already in use | Кто-то удалил контейнеры до конца сборки. Повторять через scripts/ci-deploy-compose.sh |
Картинка ![[…]] уронила проверку | Файл не закоммичен или это не картинка/PDF. Ссылки [[статья]] так не чинятся |
| PDF на сайте пустой | В iframe должен быть абсолютный /files/docs/.... Относительный files/... Docusaurus в iframe не открывает. Это чинит convert, если известен путь продукта |
| Нет страницы «Файлы» / нет zip | В папке нет orphan-вложений, или сборка не дошла до generate-files-list-sidebar |
| Admin не обновился | sync_submodule, deploy_docs, не открыт ли старый HTTP-адрес |
| User не обновился после push техписа | Так и задумано. Нужен тег docs-* |
Тег есть, publish_user нет | Тег указывает на коммит, в котором этого правила в .gitlab-ci.yml ещё не было |
| User собрался, но продукта нет | Пути нет в allowlist-public.yml |
Pipeline bump-коммита Skipped | Нормально, если trigger-pipeline выполнил deploy_docs |
| Диск 100% | Кэш BuildKit. Лимит 2 ГБ, не prune -af как постоянное решение |
| Сайт открывается без логина с голого IP по старому порту | Это не Admin. Проверьте, что смотрите sslip.io:3002 |
Часть K. Файлы, которые имеет смысл помнить
| Файл | Зачем |
|---|---|
.gitmodules | Какой репозиторий в какой папке |
.gitlab-ci.yml | Pipeline umbrella |
ci/product-trigger.gitlab-ci.yml | Общий CI продуктов |
allowlist-public.yml | Что копируется на User |
scripts/ci-sync-submodule.sh | Запись нового SHA / path-mirror |
scripts/ci-deploy-compose.sh | Сборка и запуск Admin без гонки имён |
scripts/ci-deploy-public.sh | Тег docs-* → каталог User на сервере |
scripts/convert-obsidian-embeds.sh | Форматирование картинок, SVG и PDF из Obsidian |
scripts/bump-doc-revs.cjs | Номера doc_rev при сборке |
scripts/prepare-docs-build.cjs | Копия docs → .docs-build + bump |
scripts/generate-files-list-sidebar.cjs | Страница «Файлы» и _files-all.zip |
scripts/check-doc-images.sh | Стоп публикации, если картинка битая |
scripts/backup-admin-stack.sh | Снимок перед сменой инфры |
authelia/configuration.yml | Домены логина |
users_database.yml | Логины и пароли (только на сервере) |
docker-compose.yml | Admin |
docker-compose.lockdown.yml | 3002 только на localhost |
templates/taiga-docs-user/ | Шаблон публичного сайта |
Часть L. Правила
- Токены и
.envне коммитить.users_database.ymlтоже. - SHA submodule руками не переписывать, если не хотите зафиксировать чужую версию мимо CI.
- На проде не использовать
git submodule update --remote. - Новый продукт не добавлять в
casesync-скрипта — пути считает path-mirror. - Product CI не собирает сайт.
- Менять поведение всех продуктов в одном файле:
ci/product-trigger.gitlab-ci.yml. - Правки umbrella — через Git, не правкой файлов только на сервере. Следующий
deploy_docsсделаетreset --hardи затрёт ручные правки в/opt/docusaurus-docs. - После правки CI смотреть реальный pipeline, не только зелёный diff.
- Не путать SHA техписа, SHA коммита CI в продукте и SHA umbrella.
- Техпису не нужны umbrella, submodule и сервер.
- User не обновляется push-ем техписа.
- Перед сменой портов, TLS или Authelia —
scripts/backup-admin-stack.sh. - Контейнеры Admin не удалять вручную, пока идёт сборка образа.
- Публичный allowlist править в
allowlist-public.yml, не в живой папке на сервере.
Часть M. Схема
PRODUCT main
│ push техписа
▼
convert_obsidian_embeds
│ картинки / PDF, push с ci.skip если смог
▼
trigger_docs_rebuild
│ TRIGGERED_BY + SUBMODULE_SHA
▼
UMBRELLA 177
├── sync_submodule
│ docs/<TRIGGERED_BY> = SHA, push bump с ci.skip
└── deploy_docs
reset + submodule update
проверка картинок
ci-deploy-compose.sh
prepare-docs-build (doc_rev)
«Файлы» + _files-all.zip
▼
Admin https://95.183.13.164.sslip.io:3002/
Когда нужно обновить читателя снаружи
│ тег docs-* на текущем main
▼
publish_user
│ allowlist этого тега → /opt/docusaurus-docs-user
▼
User http://95.183.13.164:3003/
Коротко:
Продукт хранит текст.
Его CI приводит вставки Obsidian к виду сайта и сообщает umbrella нужный SHA.
Umbrella запоминает этот SHA по путиdocs/<путь проекта в GitLab>.
deploy_docsсобирает именно его на внутренний сайт с логином.
Публичный сайт получает только то, что перечислено вallowlist-public.yml, и только когда на umbrella поставлен тегdocs-*.
84ad2fa